﻿namespace DataMorph.Web.Services
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Threading.Tasks;

    using DataMorph.Common;
    using DataMorph.Common.Contracts;
    using DataMorph.Common.DataAccess;
    using DataMorph.Common.Entities;
    using DataMorph.Generator;
    using DataMorph.Processing;
    using DataMorph.Web.Endpoints;
    using DataMorph.Web.Models;

    public class DataMorphService : IDataMorphService
    {
        private readonly ConcurrentDictionary<string, DataMorphServiceTask> tasks = new ConcurrentDictionary<string, DataMorphServiceTask>();

        private readonly INotifier notifier;

        private readonly IDataAccess dataAccess;

        private readonly IWorkQueue workQueue;

        private readonly IProgressQueue progressQueue;

        public DataMorphService(INotifier notifier, IDataAccess dataAccess, IWorkQueue workQueue, IProgressQueue progressQueue)
        {
            if (notifier == null)
            {
                throw new ArgumentNullException("notifier", "Notifier must not be null.");
            }

            if (dataAccess == null)
            {
                throw new ArgumentNullException("dataAccess", "Data access must not be null.");
            }

            this.notifier = notifier;
            this.dataAccess = dataAccess;
            this.workQueue = workQueue;
            this.progressQueue = progressQueue;
        }

        public string GenerateData(string clientId, string storageAccountName, GenerationParameters generationParameters)
        {
            this.notifier.Notify(clientId, new ProgressReport(0, "Initializing data generator..."));

            var targetStorageAccount = this.dataAccess.GetStorageAccount(generationParameters.StorageAccountName, generationParameters.StorageAccountKey);
            if (targetStorageAccount == null)
            {
                throw new ArgumentNullException("account", "Account must not be null.");
            }

            if (generationParameters == null)
            {
                throw new ArgumentNullException("generationParameters", "Generation parameters must not be null.");
            }

            var tagGenerator = new TagGenerator();
            var tagDataGenerator = new TagDataGenerator();
            var tagDataFileGenerator = new TagDataFileGenerator(tagDataGenerator);
            var tagDataSetGenerator =
                new TagDataSetGenerator(
                    tagDataFileGenerator, 
                    tagGenerator, 
                    (name, day) => new BlobFileWriter(targetStorageAccount, name, day));

            var dataGeneratorTask = new DataMorphServiceTask
            {
                Id = Guid.NewGuid().ToString(),
                ClientId = clientId, 
                Parameters = generationParameters,
                DataMorphTask = tagDataSetGenerator
            };

            tagDataSetGenerator.ProgressChanged += (s, a) =>
            {
                dataGeneratorTask.Progress = new ProgressReport(a.ProgressPercentage, a.UserState.ToString());
                this.notifier.Notify(dataGeneratorTask.ClientId, dataGeneratorTask.Progress);

                if (a.ProgressPercentage != 100)
                {
                    return;
                }

                DataMorphServiceTask removedTask;
                this.tasks.TryRemove(dataGeneratorTask.Id, out removedTask);
            };

            dataGeneratorTask.Task =
                Task.Factory.StartNew(() => tagDataSetGenerator.Generate(generationParameters))
                    .ContinueWith(task => this.dataAccess.AddDataSet(generationParameters));

            this.tasks.AddOrUpdate(dataGeneratorTask.Id, dataGeneratorTask, (taskId, task) => task);

            return dataGeneratorTask.Id;
        }

        public void CancelTask(string taskId)
        {
            if (string.IsNullOrEmpty(taskId))
            {
                throw new ArgumentException("Task id must not be null or empty.", "taskId");
            }

            DataMorphServiceTask task;
            if (this.tasks.TryGetValue(taskId, out task))
            {
                task.DataMorphTask.Cancel();
                this.tasks.TryRemove(task.Id, out task);
            }
        }

        public string ProcessDataSet(string clientId, GenerationParameters parameters)
        {
            this.notifier.Notify(clientId, new ProgressReport(0, "Starting work scheduler..."));

            var dataMorphTaskId = Guid.NewGuid().ToString();
            var workScheduler = new WorkScheduler(this.workQueue, this.dataAccess.GetStorageAccount);

            workScheduler.ProgressChanged += (s, a) => this.notifier.Notify(
                                                                        clientId, 
                                                                        new ProgressReport(
                                                                                    Convert.ToInt32(a.ProgressPercentage * 0.2d),
                                                                                    string.Format(
                                                                                            "{0} of {1} files scheduled for processing",
                                                                                            a.UserState,
                                                                                            parameters.NumberOfFiles)));

            var progressReader = new ProgressReader(this.progressQueue, parameters);
            progressReader.ProgressChanged += (s, a) =>
            {
                this.notifier.Notify(
                                clientId, 
                                new ProgressReport(
                                         Convert.ToInt32(0.8d * a.ProgressPercentage + 0.2d * 100),
                                         string.Format("{0} of {1} files processed", a.UserState, parameters.NumberOfFiles)));

                if (a.ProgressPercentage != 100)
                {
                    return;
                }

                progressReader.Stop();

                DataMorphServiceTask removedTask;
                this.tasks.TryRemove(dataMorphTaskId, out removedTask);
            };

            var workScheduleTask = new DataMorphServiceTask
            {
                Id = dataMorphTaskId,
                ClientId = clientId,
                Parameters = parameters,
                DataMorphTask = workScheduler,
                Task = Task.Factory.StartNew(() => workScheduler.ScheduleWork(parameters))
                                   .ContinueWith(task =>
                                       {
                                           this.notifier.Notify(clientId, new ProgressReport(20, "Starting work processors..."));
                                           progressReader.Start();
                                       })
            };

            this.tasks.AddOrUpdate(workScheduleTask.Id, workScheduleTask, (taskId, task) => task);

            return workScheduleTask.Id;
        }

        public IEnumerable<StorageAccountDetailsModel> GetStorageAccounts()
        {
            return this.dataAccess.GetStorageAccountNames();
        }

        public IEnumerable<GenerationParametersModel> GetDataSets(string accountName)
        {
            return this.dataAccess.GetDataSets(accountName).Select(GenerationParametersModel.FromDataSetEntity);
        }

        public IEnumerable<ProcessingRecord> GetProcessingInstances(string storageAccount)
        {
            const string TimeFormatString = "yyyy-MM-dd HH:mm:ss Z";

            var processingRecordEntities = this.dataAccess.GetProcessingRecordEntities(storageAccount).ToList();
            var groupedEntities = processingRecordEntities.GroupBy(x => x.PartitionKey);
            var returnValues = groupedEntities.Select(@group => new ProcessingRecord
            {
                StorageAccount = storageAccount,
                DataSet = @group.First().DataSet,
                DeploymentId = @group.First().DeploymentId,
                AverageReadTime = Math.Round(@group.Average(x => x.AverageReadTime), 2),
                AverageParseTime = Math.Round(@group.Average(x => x.AverageParseTime), 2),
                AverageWriteTime = Math.Round(@group.Average(x => x.AverageWriteTime), 2),
                MaxReadTime = Math.Round(@group.Max(x => x.MaxReadTime), 2),
                MaxParseTime = Math.Round(@group.Max(x => x.MaxParseTime), 2),
                MaxWriteTime = Math.Round(@group.Max(x => x.MaxWriteTime), 2),
                MinReadTime = Math.Round(@group.Min(x => x.MinReadTime), 2),
                MinParseTime = Math.Round(@group.Min(x => x.MinParseTime), 2),
                MinWriteTime = Math.Round(@group.Min(x => x.MinWriteTime), 2),
                FilesRead = @group.Sum(x => x.FilesRead),
                FilesWritten = @group.Sum(x => x.FilesWritten),
                TimeRequested = @group.First().TimeRequested.ToString(TimeFormatString),
                CompletedTime = @group.Max(x => x.CompletedTime).ToString(TimeFormatString),
                TotalProcessingTime = Math.Round((@group.Max(x => x.CompletedTime) - @group.First().TimeRequested).TotalSeconds, 2),
                InstanceCount = @group.Sum(x => x.InstanceCount),
                CoreCount = @group.Max(x => x.CoreCount)
            });

            return returnValues;
        }

        private class DataMorphServiceTask
        {
            public string Id { get; set; }

            public string ClientId { get; set; }

            public GenerationParameters Parameters { get; set; }

            public ICancellable DataMorphTask { get; set; }

            public Task Task { get; set; }

            public ProgressReport Progress { get; set; }
        }
    }
}